package org.n3r.idworker;

import java.security.SecureRandom;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class IdWorker {
	protected long epoch = 1288834974657L;
//    protected long epoch = 1387886498127L; // 2013-12-24 20:01:38.127

	protected long workerIdBits = 10L;
	protected long maxWorkerId = -1L ^ (-1L << workerIdBits);
	protected long sequenceBits = 11L;

	protected long workerIdShift = sequenceBits;
	protected long timestampLeftShift = sequenceBits + workerIdBits;
	protected long sequenceMask = -1L ^ (-1L << sequenceBits);

	protected long lastMillis = -1L;

	protected final long workerId;
	protected long sequence = 0L;
	protected Logger logger = LoggerFactory.getLogger(IdWorker.class);

	public IdWorker(long workerId) {
		this.workerId = checkWorkerId(workerId);

		logger.debug("worker starting. timestamp left shift {}, worker id {}", timestampLeftShift, workerId);
	}

	public long getEpoch() {
		return epoch;
	}

	private long checkWorkerId(long workerId) {
		// sanity check for workerId
		if (workerId > maxWorkerId || workerId < 0) {
			int rand = new SecureRandom().nextInt((int) maxWorkerId + 1);
			logger.warn("worker Id can't be greater than {} or less than 0, use a random {}", maxWorkerId, rand);
			return rand;
		}

		return workerId;
	}

	public synchronized long nextId() {
		long timestamp = millisGen();

		if (timestamp < lastMillis) {
			logger.error("clock is moving backwards.  Rejecting requests until {}.", lastMillis);
			throw new InvalidSystemClock(String.format(
					"Clock moved backwards.  Refusing to generate id for {} milliseconds", lastMillis - timestamp));
		}

		if (lastMillis == timestamp) {
			sequence = (sequence + 1) & sequenceMask;
			if (sequence == 0)
				timestamp = tilNextMillis(lastMillis);
		} else {
			sequence = 0;
		}

		lastMillis = timestamp;
		long diff = timestamp - getEpoch();
		return (diff << timestampLeftShift) | (workerId << workerIdShift) | sequence;
	}

	protected long tilNextMillis(long lastMillis) {
		long millis = millisGen();
		while (millis <= lastMillis)
			millis = millisGen();

		return millis;
	}

	protected long millisGen() {
		return System.currentTimeMillis();
	}

	public long getLastMillis() {
		return lastMillis;
	}

	public long getWorkerId() {
		return workerId;
	}
}
